#!/bin/sh

# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# Provides alert messages in boot stage, called by chromeos_startup.

# Two instances of this script should never be run in parallel: the alert
# animations will fight with each other, and there is a potential race in the
# emission of the boot-alert-request signal (see http://crosbug.com/33838).

# Since this script only provides messages, never abort.
set +e

# Prints usage help for commands supports
usage_help() {
  echo "Usage: $0 mode [arg ...]

  warn_dev: Message for warning about developer mode on normal firmware.
            Arg #1: (optional, default=30) Countdown in seconds before continue

  enter_dev: Message for entering developer mode from non-dev.
             Arg #1: (optional, default=30) Countdown in seconds before continue

  leave_dev: Message when leaving developer mode.
             Arg: none

  update_firmware: Message before starting firmware update.
             Arg: none

  wipe:      Message when starting to erase stateful partition.
             Arg #1: Text message file to show.

  power_wash: Message when users wants to wipe the stateful partition.
              Arg: none

  self_repair: Message when starting to rebuild stateful partition.

  dev_fwcheck: Message when staring with developer firmware, to check if there
               is available updates.

  block_devmode: Message shown to indicate that dev mode is blocked by request
                 of the device owner.
                 Arg: none
"
}

# Prints out system locale by searching cached settings or VPD.
find_current_locale() {
  # TODO(hungte) Find some better way other than hard coding file path here.
  local state_file='/mnt/stateful_partition/home/chronos/Local State'
  local locale=""
  if [ -f "$state_file" ]; then
    locale="$(grep -w '"app_locale":' "$state_file" |
              sed 's/.*"\([^"]*\)",$/\1/')" || locale=""
  fi
  if [ -z "${locale}" ]; then
    locale="$(cros_region_data -s locales)"
  fi
  echo "${locale}"
}

# Determine the right console. On Freon systems, the default TTY (/dev/tty1) may
# not exist until we've invoked Frecon (via display_boot_message) and would
# cause problems when we try to write then read (for example, warn_dev).  To
# prevent that, fall back to /dev/null if not available.
setup_tty() {
  TTY=/dev/tty1
  [ -e "${TTY}" ] || TTY=/dev/null
}

# Shows boot messages in assets folder on screen center if available.
# Arguments: message in /usr/share/chromeos-assets/text/boot_messages/$locale
show_assets_message() {
  local message="$1"
  local locale locale_list

  # Build locale list
  locale="$(find_current_locale)" || locale=""
  # Starting from R34, the initial_locale from VPD may have multiple values,
  # separated by ',' -- and we only want to try the primary one.
  locale="${locale%%,*}"
  locale_list="$locale"
  while [ "${locale%[-_]*}" != "$locale" ]; do
    locale="${locale%[-_]*}"
    locale_list="$locale_list $locale"
  done
  locale_list="$locale_list en-US en"

  if display_boot_message "${message}" "${locale_list}"; then
    # Frecon may create the text terminal so we want to setup TTY again.
    setup_tty
  else
    # Display the message code itself as fallback.
    echo "${message}" >>"${TTY}"
  fi
}

# Prints the two byte hex code of the matched char or
# exists non-zero on timeout.  It reads from the tty in arguments.
# Arguments: tty time_in_seconds two_byte_hex_match_1 two_byte_hex_match_2 ...
match_char_timeout() {
  local tty="$1"
  local delay_secs="$2"
  shift
  shift

  local input=''
  local match=''
  local start_time=$(date +%s)
  local stop_time=$((start_time + delay_secs))
  local tty_config=$(stty -g -F "${tty}")
  stty raw -echo cread -F "${tty}"
  while [ $delay_secs -gt 0 ]; do
    input=$(timeout -s KILL ${delay_secs}s head -c 1 "${tty}")
    [ $? -eq 137 ] && break  # Timed out.
    input=$(printf "%02x" "'$input")
    for char in "$@"; do
      if [ "$input" = "$char" ]; then
        match="$input"
        break
      fi
    done
    [ -n "$match" ] && break
    delay_secs=$((stop_time - $(date +%s) ))
  done
  # Restores the tty's settings.
  stty $tty_config -F "${tty}"

  [ -z "$match" ] && return 1
  printf "$match"
  return 0
}

# Returns if current device is using virtual developer switch.
has_virtual_dev_switch() {
  local VBSD_HONOR_VIRT_DEV_SWITCH="0x400"
  local vdat_flags="$(crossystem vdat_flags || echo 0)"
  [ "$((vdat_flags & VBSD_HONOR_VIRT_DEV_SWITCH))" != "0" ]
}

# Prints message when in developer mode
# Argument: time to countdown (in seconds)
mode_warn_dev() {
  local delay_secs="${1:-30}"
  # In bash and dash, the "NAME=VAL program" will set a temporary environment
  # variable. However if the "program" is a shell function and then calls a
  # standalone shell script, only bash can pass the variable properly.
  # To work around that, here we have to explicitly set the variable with
  # "export" and use a sub shell.
  (export IMAGE_FONT_NAME="monospace"; show_assets_message "warn_dev")

  # Read a space bar or Ctrl+D or timeout.
  local input=$(match_char_timeout "${TTY}" "$delay_secs" 04 20)
  local exit_code=$?
  tput clear >>"${TTY}"
  # If we timed out, we're done.
  [ $exit_code -ne 0 ] && return 0
  case "$input" in
    "04")  # Ctrl+D
      # Done.
      ;;
    "20")  # Spacebar
      crossystem recovery_request=1
      reboot
      # To prevent the system from continuing to boot.
      sleep infinity
      ;;
  esac
  return 0
}


# Prints message when entering developer mode
# Argument: time to countdown (in seconds)
mode_enter_dev() {
  local delay_secs="${1:-30}"

  if has_virtual_dev_switch; then
    show_assets_message "enter_dev1_virtual"
  else
    show_assets_message "enter_dev1"
  fi

  local format='\r  %-30s'
  for dev_count_down in $(seq $delay_secs -1 1); do
    # Trailing spaces must exist to clear previous message when the printed
    # counter width changed (ex, 100->99).
    # TODO(hungte) merge this with assets messages so it can be localized.
    printf "$format" "Starting in $dev_count_down second(s)..." >>"${TTY}"
    sleep 1
  done

  # Count-down
  tput clear >>"${TTY}"
  show_assets_message "enter_dev2"
  # TODO(wad,wfrichar) Request a root password here.
  # TODO(wad,reinauer) Inform the user of chromeos-firmwareupdate --mode=todev
}

# Prints message when leaving developer mode
mode_leave_dev() {
  show_assets_message "leave_dev"
}

# Prints messages before starting firmware update
mode_update_firmware() {
  show_assets_message "update_firmware"
}

# Prints message before starting to erase stateful partition (wipe).
# Argument: Text file for message to show.
mode_wipe() {
  local message_file="$1"

  if display_boot_message "show_spinner" "${message_file}"; then
    # Frecon may create the text terminal so we want to setup TTY again.
    setup_tty
  else
    echo "Wiping" >>"${TTY}"
  fi
}

# Prints messages before starting user-initiated wipe.
mode_power_wash() {
  show_assets_message "power_wash"
}


# Prints message before starting to rebuild a corrupted stateful partition.
mode_self_repair() {
  show_assets_message "self_repair"
}

# Prints message when starting with developer firmware, to check if there's
# available updates for firmware.
mode_dev_fwcheck() {
  # TODO(hungte) Find some better way to store firmware value, like using
  # /etc/lsb-release.  Currently the firmware updater may contain fields like
  # TARGET_* in the beginning of updater script.
  local TARGET_FWID=""
  local TARGET_ECID=""
  # The magic value 40 is verified on current updaters (using line 23-24)
  eval "$(head -40 /usr/sbin/chromeos-firmwareupdate |
          grep '^ *TARGET_..ID=')"
  if [ -z "$TARGET_FWID" -a -z "$TARGET_ECID" ]; then
    return
  fi

  local fwid="$(crossystem fwid)"
  local ec_info
  ec_info="$(mosys -k ec info 2>/dev/null)" || ec_info=""
  local ecid="$(fw_version="Unknown"; eval "$ec_info"; echo "$fw_version")"

  # Ignore known firmware images carried by updaters with multiple images
  # TODO(hungte) Replace this by a "compatible list" in updater if such request
  # becomes a common feature.
  case "$(mosys platform name 2>/dev/null):$ecid" in
    "ZGB:0.14" | "Alex:00VFA616" )
      TARGET_ECID="$ecid"
      ;;
  esac

  local notify_update=0
  if [ "$TARGET_FWID" != "$fwid" ] &&
     [ "$TARGET_FWID" != "IGNORE" ]; then
    notify_update=1
    echo "
      System firmware update available: [$TARGET_FWID]
      Currently installed system firmware: [$fwid]
    " >>"${TTY}"
  fi
  if [ "$TARGET_ECID" != "$ecid" ] &&
     [ "$TARGET_ECID" != "IGNORE" ]; then
    notify_update=1
    echo "
      EC firmware update available: [$TARGET_ECID]
      Currently installed EC firmware: [$ecid]
    " >>"${TTY}"
  fi
  if [ $notify_update -ne 0 ]; then
    echo "
      Firmware auto updating is disabled for developer mode. If you want to
      manually update your firmware, please run the following command from a
      root shell:

      sudo chromeos-firmwareupdate --force --mode=recovery
    " >>"${TTY}"
  fi
}

# Prints a message telling the user that developer mode has been disabled for
# the device upon request by the device owner. Depending on hardware support,
# the system switches back to verified boot mode automatically or prompts the
# user to do so. The system reboots after the user confirms by pressing space or
# after timeout.
mode_block_devmode() {
  local delay_secs=30

  if has_virtual_dev_switch; then
    show_assets_message "block_devmode_virtual"
  else
    # Leave the notification on the screen for 5 minutes to increase chances
    # the user actually sees it before shutting down the device.
    delay_secs=300
    show_assets_message "block_devmode"
  fi

  # Read a space bar or timeout.
  local input=$(match_char_timeout "${TTY}" "${delay_secs}" 20)
  tput clear >>"${TTY}"

  if has_virtual_dev_switch; then
    # Return to verified mode.
    crossystem disable_dev_request=1
    reboot
  else
    # Shut down instead of rebooting in a loop.
    halt
  fi

  # To prevent the system from continuing to boot.
  sleep infinity
}

# Main initialization and dispatcher
main() {
  # process args
  if [ $# -lt 1 ]; then
    usage_help
    exit 1
  fi
  local mode="$1"
  shift

  # For headless devices, we want to provide some messages so people can know
  # what goes wrong.
  local output
  for output in /dev/kmsg /dev/console; do
    (echo "chromeos-boot-alert: ${mode}" >>"${output}") 2>/dev/null || true
  done

  setup_tty

  if [ -x /usr/sbin/board_boot_alert ]; then
    # Allow overriding boot-alert behavior (usually for headless devices).
    /usr/sbin/board_boot_alert "${mode}" "$@" && exit
  fi

  # Wait until the boot-alert-ready abstract job has started, indicating that
  # it's safe to display an alert onscreen.
  if ! initctl status boot-alert-ready | grep -q running; then
    initctl emit boot-alert-request
  fi

  # Light up the screen if possible.
  backlight_tool --set_brightness_percent=100 || true

  # Hides cursor and prevents console from blanking after long inactivity.
  setterm -cursor off -blank 0 -powersave off -powerdown 0 >>"${TTY}" || true

  case "$mode" in
    "warn_dev" | "enter_dev" | "leave_dev" | "dev_fwcheck" | \
      "update_firmware" | "wipe" | "power_wash" | "self_repair" | \
      "block_devmode" )
      mode_"$mode" "$@"
      ;;
    * )
      usage_help
      exit 1
      ;;
  esac
}

# Main Entry
main "$@"
